import sys
from re import compile
from xml.dom import minidom
from xml.sax.saxutils import escape
import sympy
import keyword

class Executor:
    objectPattern = compile(r'__objects__\[\d+\]')
    log = []
    
    def __init__(self, settings, parsers):
        self.parsers = parsers
        self.settings = settings
        self.objects = []
        self.setGlobalNamespace()
        self.pythonKeyword = keyword.kwlist
        
    def setGlobalNamespace(self):
        '''
        Set's the global Environment the user is working in.
        '''
        
        #import the standard sympy module
        import sympy
        reload(sympy)
        
        self.sympyPlot = sympy.Plot
        sympy.Plot = self._plot
        
        #bind the user identifiers to the sympy functions
        for i in ('sympyFunction', 'sympyConstant'):
            for id, key in self.settings.keywords[i]:
                exec '%s=sympy.%s'%(id, key)
        
        #now some additional functions are defined
        evalf = self._method_evalf
        show = self._show
        clear = self._clear
        __defineVariables__ = self._defineVariables
            
        #now the namespace is created
        self.globalNamespace = locals()
        
        #but there really should be no self inside! 
        del self.globalNamespace['self']
        
        #reset the sympy plot function
        sympy.Plot = self.sympyPlot
    
    def execute(self, text, localNamespace):
        self.localNamespace = localNamespace
        
        #override sympyPlot with own Plot method, since the same module is used
        #everywhere, we can do this here instead inside the namespace
        
        #do the preparsing
        text = self.parse(text)
        
        #switch the standard output to the log list
        self.log = []
        sys.stdout = self
        
        
        #run the commands though the python-interpreter
        try:
            exec text in self.globalNamespace, localNamespace
        except:
            self.objects.append('<symbide_error>%s</symbide_error>'%escape(str(sys.exc_value)))
            self.log.append('__objects__[%s]\n'%str(len(self.objects)-1))
            
        #reset the standandard output
        sys.stdout = sys.__stdout__
        
        text = ''.join(self.log[:-1])
        
        #first escape the text then fill in the objects
        text = escape(text)
        def subst(word):
            name = word.group()
            p1 = name.find('[')+1
            p2 = name.find(']')
            return self.objects[int(name[p1:p2])]
        text = self.objectPattern.sub(subst, text)
        self.objects[:] = []
        
        return minidom.parseString('<output>%s</output>'%text).documentElement
    
    def write(self, data):
        self.log.append(data)
        
    def parse(self, text):
        '''
        Carries out the parsing from plain text to excuteable text.
        
        This process is structured as follows:
            - escape all strings, so they won't be deformed by further parsing
            - encapsulate all numbers with sympy.Number
            - split text up in single statements
            - define before each statement all not yet declared keywords
            - unescape all strings again
        '''
        
        text = text.expandtabs(int(self.settings.options['tabswidth']))
        text = self.parsers['string'].escape(text)
        statements = self.parsers['statement'].parse(text)
        
        buf = []
        for statement in statements:
            possibleVariables = self.settings.pattern['variable'].findall(statement[1])
            indent = self._getIndent(''.join(statement))
            funcdef = ' '*indent+'__defineVariables__(globals(), locals(), *%s)'
            buf.append(funcdef%possibleVariables)
            if statement[0]:
                buf.append('='.join(statement))
            else:
                buf.append(statement[1])
        text = self.parsers['number'].parse('\n'.join(buf))
        text = self.parsers['string'].unescape(text)
        
        return text
    
    def _getIndent(self, text):
        '''
        Returns the count of whitespaces (after converting tabs) at the 
        beginning of text.
        '''
        
        i = 0
        for char in text:
            if char!=' ':
                break
            i+=1
        return i
    
    def _defineVariables(self, globalNamespace, localNamespace, *args):
        '''
        Declare all not yet defined variables as sympy.Symbols
        '''
        
        for i in args:
            if i in globalNamespace.keys():
                continue
            if i in localNamespace.keys():
                continue
            if i in self.pythonKeyword:
                continue
            if i in dir(__builtins__):
                continue
            localNamespace[i]=sympy.Symbol(str(i))
    
    def _method_evalf(self, x):
        return x.evalf()
    
    def _show(self, plot):
        xml = self.parsers['plot'].plot_to_xml(plot)
        self.objects.append(xml)
        self.log.append('__objects__[%s]\n'%str(len(self.objects)-1))
        
    def _clear(self, *args):
        '''
        Deletes all given arguments out of the local namespace.
        If called with no arguments, the dictionary is emptied completely.
        '''
        
        if args:
            for key, item in self.localNamespace.items():
                if item in args:
                    del self.localNamespace[key]
        else:
            self.localNamespace.clear()
            
    def _plot(self, *args, **kwargs):
        kwargs.update({'visible':False})
        p = self.sympyPlot(*args, **kwargs)
        setattr(p, 'show', self._method_show)
        return p
    
    def _method_show(self):
        print 'this method is not implemented in symbide'
